﻿// (c) Copyright Microsoft Corporation.   
// This source is subject to the Microsoft Public License (Ms-PL).   
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.   
// All other rights reserved.   

using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Imaging;

// Learn more about Gravatar online:
// - Setup your avatar: http://www.gravatar.com/
// - Implementation details: http://en.gravatar.com/site/implement/url

// This control is not associated with Gravatar in any way, and is provided 
// as-is. Enjoy!

namespace Controls
{
    /// <summary>
    /// A simple image control for displaying a Gravatar, or globally recognized
    /// avatar, based on the hash of the Email dependency property value.
    /// </summary>
    public partial class Gravatar : Control
    {
        #region Constants and template parts
        /// <summary>
        /// The name of the Image part.
        /// </summary>
        private const string ImagePartName = "Image";

        /// <summary>
        /// The default to provide to Gravatar, either an image URI or a special
        /// mode such as identicon.
        /// </summary>
        private const string DefaultImage = "identicon";

        /// <summary>
        /// The image part.
        /// </summary>
        private Image _image;
        #endregion

        /// <summary>
        /// The current rating value.
        /// </summary>
        private string _rating;

        #region public string Default
        /// <summary>
        /// Gets or sets the default image URI or special operation mode. At the
        /// time of this control's development, special modes supported included
        /// identicon, monsterid, and wavatar. This URI or operation mode is
        /// used only when the hashed email value is found to not have a
        /// Gravatar.
        /// </summary>
        public string Default
        {
            get { return GetValue(DefaultProperty) as string; }
            set { SetValue(DefaultProperty, value); }
        }

        /// <summary>
        /// Identifies the Default dependency property.
        /// </summary>
        public static readonly DependencyProperty DefaultProperty =
            DependencyProperty.Register(
                "Default",
                typeof(string),
                typeof(Gravatar),
                new PropertyMetadata(DefaultImage, OnDefaultPropertyChanged));

        /// <summary>
        /// DefaultProperty property changed handler.
        /// </summary>
        /// <param name="d">Gravatar that changed its Default.</param>
        /// <param name="e">Event arguments.</param>
        private static void OnDefaultPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Gravatar source = d as Gravatar;
            source.UpdateGravatar();
        }
        #endregion public string Default

        #region public string Email
        /// <summary>
        /// Gets or sets the e-mail address of the gravatar. Can also contain a
        /// unique identifier, IP address, or any other data. This value is used
        /// through the hash algorithm to retrieve a Gravatar, if available.
        /// </summary>
        public string Email
        {
            get { return GetValue(EmailProperty) as string; }
            set { SetValue(EmailProperty, value); }
        }

        /// <summary>
        /// Identifies the Email dependency property.
        /// </summary>
        public static readonly DependencyProperty EmailProperty =
            DependencyProperty.Register(
                "Email",
                typeof(string),
                typeof(Gravatar),
                new PropertyMetadata(string.Empty, OnEmailPropertyChanged));

        /// <summary>
        /// EmailProperty property changed handler.
        /// </summary>
        /// <param name="d">Gravatar that changed its Email.</param>
        /// <param name="e">Event arguments.</param>
        private static void OnEmailPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Gravatar source = d as Gravatar;
            source.UpdateGravatar();
        }
        #endregion public string Email

        #region public GravatarRating MaximumRating
        /// <summary>
        /// Gets or sets the maximum accepted rating to accept for Gravatar 
        /// images.
        /// </summary>
        public GravatarRating MaximumRating
        {
            get { return (GravatarRating)GetValue(MaximumRatingProperty); }
            set { SetValue(MaximumRatingProperty, value); }
        }

        /// <summary>
        /// Identifies the MaximumRating dependency property.
        /// </summary>
        public static readonly DependencyProperty MaximumRatingProperty =
            DependencyProperty.Register(
                "MaximumRating",
                typeof(GravatarRating),
                typeof(Gravatar),
                new PropertyMetadata(GravatarRating.Undefined, OnMaximumRatingPropertyChanged));

        /// <summary>
        /// MaximumRatingProperty property changed handler.
        /// </summary>
        /// <param name="d">Gravatar that changed its MaximumRating.</param>
        /// <param name="e">Event arguments.</param>
        private static void OnMaximumRatingPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Gravatar source = d as Gravatar;
            GravatarRating rating = (GravatarRating) e.NewValue;
            if (rating != GravatarRating.Undefined &&
                rating != GravatarRating.G &&
                rating != GravatarRating.PG &&
                rating != GravatarRating.R &&
                rating != GravatarRating.X)
            {
                source.MaximumRating = (GravatarRating)e.OldValue;
                throw new ArgumentOutOfRangeException("MaximumRating");
            }

            string r = string.Empty;
            switch (rating)
            {
                case GravatarRating.G:
                    r = "g";
                    break;

                case GravatarRating.PG:
                    r = "pg";
                    break;

                case GravatarRating.R:
                    r = "r";
                    break;

                case GravatarRating.X:
                    r = "x";
                    break;

                default:
                case GravatarRating.Undefined:
                    r = string.Empty;
                    break;
            }
            source._rating = r;
            source.UpdateGravatar();
        }
        #endregion public GravatarRating MaximumRating


        #region EmailMD5Hash (DependencyProperty)

        /// <summary>
        /// The MD5 encoded email address.
        /// </summary>
        public string EmailMD5Hash
        {
            get { return (string)GetValue(EmailMD5HashProperty); }
            set { SetValue(EmailMD5HashProperty, value); }
        }
        public static readonly DependencyProperty EmailMD5HashProperty =
            DependencyProperty.Register("EmailMD5Hash", typeof(string), typeof(Gravatar),
              new PropertyMetadata(string.Empty, OnEmailMD5HashPropertyChanged));

        private static void OnEmailMD5HashPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Gravatar source = d as Gravatar;
            source.UpdateGravatar();
        }

        #endregion


#if !SILVERLIGHT
        /// <summary>
        /// Static initializer for the Gravatar type.
        /// </summary>
        [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", Justification = "Dependency properties are initialized in-line.")]
        static Gravatar()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(Gravatar), new FrameworkPropertyMetadata(typeof(Gravatar)));
        }
#endif

        /// <summary>
        /// Initializes a new instance of the Gravatar control.
        /// </summary>
        public Gravatar()
        {
#if SILVERLIGHT
            DefaultStyleKey = typeof(Gravatar);
#endif
            Loaded += (o, e) => ApplyTemplate();
        }

        /// <summary>
        /// Overrides the OnApplyTemplate method to retrieve template parts.
        /// </summary>
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            if (_image != null)
            {
                _image.ImageFailed -= OnImageFailed;
            }

            // Grab the defined Image from the template
            _image = GetTemplateChild(ImagePartName) as Image;

            if (_image != null)
            {
                _image.ImageFailed += OnImageFailed;
                UpdateGravatar();
            }
        }

        /// <summary>
        /// Handles the failed image routine.
        /// </summary>
        /// <param name="sender">The source object.</param>
        /// <param name="e">The event data.</param>
        protected virtual void OnImageFailed(object sender, ExceptionRoutedEventArgs e)
        {
            try
            {
                RevertToDefault();
            }
            finally
            {
            }
        }

        /// <summary>
        /// Called when changes cause the Gravatar to need an update.
        /// </summary>
        protected void UpdateGravatar()
        {
            if (_image == null)
            {
                return;
            }

            if (string.IsNullOrEmpty(Email) && string.IsNullOrEmpty(EmailMD5Hash))
            {
                RevertToDefault();
                return;
            }

            _image.Source = new BitmapImage(GetGravatarUri());
        }

        /// <summary>
        /// Reverts back to a simple default image. This is different than the 
        /// server-provided default/custom images, since it assumes that the 
        /// request could not be completed.
        /// </summary>
        private void RevertToDefault()
        {
            Assembly assembly = typeof(Gravatar).Assembly;
            BitmapImage source = null;
            using (Stream resource = assembly.GetManifestResourceStream("Silverlight.Weblog.Client.Default.Widgets.Comments.Gravatar.Default.jpg"))
            {
                if (resource != null)
                {
                    source = new BitmapImage();
                    source.SetSource(resource);
                }
            }
            _image.Source = source;
        }

        /// <summary>
        /// Retrieves a Gravatar Uri for the current property values.
        /// </summary>
        /// <returns>Returns a new Uri.</returns>
        protected Uri GetGravatarUri()
        {
            // Reference: http://en.gravatar.com/site/implement/url
            StringBuilder sb = new StringBuilder();

            sb.Append("http://www.gravatar.com/avatar/");

            if (!string.IsNullOrEmpty(EmailMD5Hash))
                sb.Append(EmailMD5Hash);
            else
                sb.Append(Md5EncodeText(Email ?? string.Empty));
            
            sb.Append(".jpg");

            // Size
            sb.Append("?s=");
            int width = (int) Math.Round(Width);
            sb.Append(width);

            // Rating
            if (!string.IsNullOrEmpty(_rating))
            {
                sb.Append("&r=");
                sb.Append(_rating);
            }

            // Default and special modes
            if (!string.IsNullOrEmpty(Default))
            {
                sb.Append("&d=");
                sb.Append(Default.UrlEncode());
            }

            return new Uri(sb.ToString());
        }

        /// <summary>
        /// Retrieves the MD5 encoded hash string of the provided input.
        /// </summary>
        /// <param name="text">The input text.</param>
        /// <returns>Returns the MD5 hash string of the input string.</returns>
        protected string Md5EncodeText(string text)
        {
            StringBuilder sb = new StringBuilder();

            byte[] ss = 
                new MD5Managed()
                .ComputeHash(Encoding.UTF8.GetBytes(text));
            foreach (byte b in ss)
            {
                sb.Append(b.ToString("X2"));
            }
            return sb.ToString().ToLower();
        }
    }
}